שלטו באיטרציה אסינכרונית ב-JavaScript באמצעות לולאת 'for await...of' ועזרי איטרטור אסינכרוני מותאמים אישית. שפרו עיבוד זרמי נתונים וטיפול במידע עם דוגמאות מעשיות.
עזר איטרטור אסינכרוני ב-JavaScript: For Each - איטרציה לעיבוד זרמי נתונים
תכנות אסינכרוני הוא אבן יסוד בפיתוח JavaScript מודרני, המאפשר לאפליקציות לטפל בפעולות שגוזלות זמן מבלי לחסום את התהליך הראשי (main thread). איטרטורים אסינכרוניים, שהוצגו ב-ECMAScript 2018, מספקים מנגנון רב עוצמה לעיבוד זרמי נתונים באופן אסינכרוני. פוסט זה צולל למושג של איטרטורים אסינכרוניים ומדגים כיצד לממש פונקציית עזר אסינכרונית מסוג 'for each' כדי לייעל את עיבוד הזרמים.
הבנת איטרטורים אסינכרוניים
איטרטור אסינכרוני הוא אובייקט התואם לממשק AsyncIterator. הוא מגדיר מתודה בשם next() שמחזירה promise, אשר נפתרת (resolves) לאובייקט עם שתי תכונות:
value: הערך הבא ברצף.done: ערך בוליאני המציין אם האיטרטור סיים את פעולתו.
איטרטורים אסינכרוניים משמשים בדרך כלל לצריכת נתונים ממקורות אסינכרוניים כמו זרמי רשת, מערכות קבצים או מסדי נתונים. לולאת for await...of מספקת תחביר נוח לאיטרציה על פני איטרבילים (iterables) אסינכרוניים.
דוגמה: קריאה אסינכרונית מקובץ
שקלו תרחיש שבו אתם צריכים לקרוא קובץ גדול שורה אחר שורה מבלי לחסום את התהליך הראשי. ניתן להשיג זאת באמצעות איטרטור אסינכרוני:
const fs = require('fs');
const readline = require('readline');
async function* readFileLines(filePath) {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
for await (const line of rl) {
yield line;
}
}
async function processFile(filePath) {
for await (const line of readFileLines(filePath)) {
console.log(`Line: ${line}`);
}
}
// Example usage
processFile('path/to/your/file.txt');
בדוגמה זו, readFileLines היא פונקציית גנרטור אסינכרונית שמניבה (yields) כל שורת קובץ בזמן שהיא נקראת. לאחר מכן, פונקציית processFile מבצעת איטרציה על השורות באמצעות for await...of, ומעבדת כל שורה באופן אסינכרוני.
מימוש פונקציית עזר אסינכרונית מסוג 'For Each'
בעוד שלולאת for await...of שימושית, היא עלולה להפוך למסורבלת כאשר יש צורך לבצע פעולות מורכבות על כל אלמנט בזרם. פונקציית עזר אסינכרונית מסוג 'for each' יכולה לפשט תהליך זה על ידי כימוס (encapsulating) לוגיקת האיטרציה.
מימוש בסיסי
הנה מימוש בסיסי של פונקציית 'for each' אסינכרונית:
async function asyncForEach(iterable, callback) {
for await (const item of iterable) {
await callback(item);
}
}
פונקציה זו מקבלת איטרביל אסינכרוני ופונקציית callback כארגומנטים. היא מבצעת איטרציה על האיטרביל באמצעות for await...of וקוראת לפונקציית ה-callback עבור כל פריט. פונקציית ה-callback צריכה להיות גם היא אסינכרונית אם ברצונכם להמתין לסיומה לפני המעבר לפריט הבא.
דוגמה: עיבוד נתונים מ-API
נניח שאתם מושכים נתונים מ-API שמחזיר זרם של פריטים. תוכלו להשתמש בפונקציית העזר האסינכרונית 'for each' כדי לעבד כל פריט עם הגעתו:
async function* fetchDataStream(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
return;
}
// Assuming the API returns JSON chunks
const chunk = decoder.decode(value);
const items = JSON.parse(`[${chunk.replace(/\}\{/g, '},{')}]`); //Split chunks into valid json array
for(const item of items){
yield item;
}
}
} finally {
reader.releaseLock();
}
}
async function processItem(item) {
// Simulate an asynchronous operation
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`Processing item: ${JSON.stringify(item)}`);
}
async function main() {
const apiUrl = 'https://api.example.com/data'; // Replace with your API endpoint
await asyncForEach(fetchDataStream(apiUrl), processItem);
console.log('Finished processing data.');
}
// Example usage
main();
בדוגמה זו, fetchDataStream מושכת נתונים מה-API ומניבה כל פריט עם קבלתו. פונקציית processItem מדמה פעולה אסינכרונית על כל פריט. לאחר מכן, פונקציית העזר asyncForEach מפשטת את לוגיקת האיטרציה והעיבוד.
שיפורים ושיקולים
טיפול בשגיאות
חיוני לטפל בשגיאות שעלולות להתרחש במהלך איטרציה אסינכרונית. ניתן לעטוף את פונקציית ה-callback בבלוק try...catch כדי לתפוס ולטפל בחריגות (exceptions):
async function asyncForEach(iterable, callback) {
for await (const item of iterable) {
try {
await callback(item);
} catch (error) {
console.error(`Error processing item: ${item}`, error);
// You can choose to re-throw the error or continue processing
}
}
}
בקרת מקביליות (Concurrency)
כברירת מחדל, פונקציית העזר האסינכרונית 'for each' מעבדת פריטים באופן סדרתי. אם אתם צריכים לעבד פריטים במקביל, תוכלו להשתמש במאגר של Promises (Promise pool) כדי להגביל את מספר הפעולות המקביליות:
async function asyncForEachConcurrent(iterable, callback, concurrency) {
const executing = [];
for await (const item of iterable) {
const p = callback(item).then(() => executing.splice(executing.indexOf(p), 1));
executing.push(p);
if (executing.length >= concurrency) {
await Promise.race(executing);
}
}
await Promise.all(executing);
}
async function processItem(item) {
// Simulate an asynchronous operation
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`Processing item: ${JSON.stringify(item)}`);
}
async function main() {
const apiUrl = 'https://api.example.com/data'; // Replace with your API endpoint
await asyncForEachConcurrent(fetchDataStream(apiUrl), processItem, 5); // Concurrency of 5
console.log('Finished processing data.');
}
בדוגמה זו, asyncForEachConcurrent מגבילה את מספר ביצועי ה-callback המקביליים לרמת המקביליות שצוינה. זה יכול לשפר את הביצועים כאשר מתמודדים עם זרמי נתונים גדולים.
ביטול
במקרים מסוימים, ייתכן שתצטרכו לבטל את תהליך האיטרציה בטרם עת. ניתן להשיג זאת באמצעות AbortController:
async function asyncForEach(iterable, callback, signal) {
for await (const item of iterable) {
if (signal && signal.aborted) {
console.log('Iteration aborted.');
return;
}
await callback(item);
}
}
async function main() {
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => {
controller.abort(); // Abort after 2 seconds
}, 2000);
const apiUrl = 'https://api.example.com/data'; // Replace with your API endpoint
await asyncForEach(fetchDataStream(apiUrl), processItem, signal);
console.log('Finished processing data.');
}
בדוגמה זו, פונקציית asyncForEach בודקת את המאפיין signal.aborted לפני כל איטרציה. אם האות (signal) בוטל, האיטרציה נעצרת.
יישומים בעולם האמיתי
איטרטורים אסינכרוניים ופונקציית העזר האסינכרונית 'for each' ניתנים ליישום במגוון רחב של תרחישים בעולם האמיתי:
- צנרת עיבוד נתונים: עיבוד מערכי נתונים גדולים ממסדי נתונים או מערכות קבצים.
- זרמי נתונים בזמן אמת: טיפול בנתונים מ-web sockets, תורי הודעות, או רשתות חיישנים.
- צריכת API: משיכה ועיבוד של נתונים מממשקי API המחזירים זרמי פריטים.
- עיבוד תמונה ווידאו: עיבוד קבצי מדיה גדולים במקטעים (chunks).
- ניתוח לוגים: ניתוח קבצי לוג גדולים שורה אחר שורה.
דוגמה - נתוני מניות בינלאומיים: שקלו אפליקציה המושכת ציטוטי מניות בזמן אמת מבורסות בינלאומיות שונות. ניתן להשתמש באיטרטור אסינכרוני כדי להזרים את הנתונים, ופונקציית 'for each' אסינכרונית יכולה לעבד כל ציטוט, ולעדכן את ממשק המשתמש במחירים העדכניים ביותר. ניתן להשתמש בזה כדי להציג שערי מניות עדכניים של חברות כמו:
- Tencent (סין): משיכת נתוני מניה של חברת טכנולוגיה בינלאומית גדולה
- Tata Consultancy Services (הודו): הצגת עדכוני מניות מחברת שירותי IT מובילה
- Samsung Electronics (דרום קוריאה): הצגת שערי מניות מיצרנית אלקטרוניקה עולמית
- Toyota Motor Corporation (יפן): ניטור מחירי מניות של יצרנית רכב בינלאומית
סיכום
איטרטורים אסינכרוניים ופונקציית העזר האסינכרונית 'for each' מספקים דרך חזקה ואלגנטית לעבד זרמי נתונים באופן אסינכרוני ב-JavaScript. על ידי כימוס לוגיקת האיטרציה, תוכלו לפשט את הקוד שלכם, לשפר את הקריאות ולהגביר את ביצועי האפליקציות שלכם. על ידי טיפול בשגיאות, בקרת מקביליות ומתן אפשרות לביטול, תוכלו ליצור צנרת עיבוד נתונים אסינכרונית חזקה וסקיילבילית.